// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>

#include "base/command_line.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/site_isolation_policy.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/url_request/url_request_failed_job.h"
#include "url/gurl.h"

namespace content {

class BrowserSideNavigationBrowserTest : public ContentBrowserTest {
public:
    BrowserSideNavigationBrowserTest() { }

protected:
    void SetUpCommandLine(base::CommandLine* command_line) override
    {
        command_line->AppendSwitch(switches::kEnableBrowserSideNavigation);
    }

    void SetUpOnMainThread() override
    {
        host_resolver()->AddRule("*", "127.0.0.1");
        ASSERT_TRUE(embedded_test_server()->Start());
    }
};

// Ensure that browser initiated basic navigations work with browser side
// navigation.
IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest,
    BrowserInitiatedNavigations)
{
    // Perform a navigation with no live renderer.
    {
        TestNavigationObserver observer(shell()->web_contents());
        GURL url(embedded_test_server()->GetURL("/title1.html"));
        NavigateToURL(shell(), url);
        EXPECT_EQ(url, observer.last_navigation_url());
        EXPECT_TRUE(observer.last_navigation_succeeded());
    }

    RenderFrameHost* initial_rfh = static_cast<WebContentsImpl*>(shell()->web_contents())
                                       ->GetFrameTree()
                                       ->root()
                                       ->current_frame_host();

    // Perform a same site navigation.
    {
        TestNavigationObserver observer(shell()->web_contents());
        GURL url(embedded_test_server()->GetURL("/title2.html"));
        NavigateToURL(shell(), url);
        EXPECT_EQ(url, observer.last_navigation_url());
        EXPECT_TRUE(observer.last_navigation_succeeded());
    }

    // The RenderFrameHost should not have changed.
    EXPECT_EQ(initial_rfh, static_cast<WebContentsImpl*>(shell()->web_contents())->GetFrameTree()->root()->current_frame_host());

    // Perform a cross-site navigation.
    {
        TestNavigationObserver observer(shell()->web_contents());
        GURL url = embedded_test_server()->GetURL("foo.com", "/title3.html");
        NavigateToURL(shell(), url);
        EXPECT_EQ(url, observer.last_navigation_url());
        EXPECT_TRUE(observer.last_navigation_succeeded());
    }

    // The RenderFrameHost should have changed.
    EXPECT_NE(initial_rfh, static_cast<WebContentsImpl*>(shell()->web_contents())->GetFrameTree()->root()->current_frame_host());
}

// Ensure that renderer initiated same-site navigations work with browser side
// navigation.
IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest,
    RendererInitiatedSameSiteNavigation)
{
    // Perform a navigation with no live renderer.
    {
        TestNavigationObserver observer(shell()->web_contents());
        GURL url(embedded_test_server()->GetURL("/simple_links.html"));
        NavigateToURL(shell(), url);
        EXPECT_EQ(url, observer.last_navigation_url());
        EXPECT_TRUE(observer.last_navigation_succeeded());
    }

    RenderFrameHost* initial_rfh = static_cast<WebContentsImpl*>(shell()->web_contents())
                                       ->GetFrameTree()
                                       ->root()
                                       ->current_frame_host();

    // Simulate clicking on a same-site link.
    {
        TestNavigationObserver observer(shell()->web_contents());
        GURL url(embedded_test_server()->GetURL("/title2.html"));
        bool success = false;
        EXPECT_TRUE(ExecuteScriptAndExtractBool(
            shell(), "window.domAutomationController.send(clickSameSiteLink());",
            &success));
        EXPECT_TRUE(success);
        EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
        EXPECT_EQ(url, observer.last_navigation_url());
        EXPECT_TRUE(observer.last_navigation_succeeded());
    }

    // The RenderFrameHost should not have changed.
    EXPECT_EQ(initial_rfh, static_cast<WebContentsImpl*>(shell()->web_contents())->GetFrameTree()->root()->current_frame_host());
}

// Ensure that renderer initiated cross-site navigations work with browser side
// navigation.
IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest,
    RendererInitiatedCrossSiteNavigation)
{
    // Perform a navigation with no live renderer.
    {
        TestNavigationObserver observer(shell()->web_contents());
        GURL url(embedded_test_server()->GetURL("/simple_links.html"));
        NavigateToURL(shell(), url);
        EXPECT_EQ(url, observer.last_navigation_url());
        EXPECT_TRUE(observer.last_navigation_succeeded());
    }

    RenderFrameHost* initial_rfh = static_cast<WebContentsImpl*>(shell()->web_contents())
                                       ->GetFrameTree()
                                       ->root()
                                       ->current_frame_host();

    // Simulate clicking on a cross-site link.
    {
        TestNavigationObserver observer(shell()->web_contents());
        const char kReplacePortNumber[] = "window.domAutomationController.send(setPortNumber(%d));";
        uint16_t port_number = embedded_test_server()->port();
        GURL url = embedded_test_server()->GetURL("foo.com", "/title2.html");
        bool success = false;
        EXPECT_TRUE(ExecuteScriptAndExtractBool(
            shell(), base::StringPrintf(kReplacePortNumber, port_number),
            &success));
        success = false;
        EXPECT_TRUE(ExecuteScriptAndExtractBool(
            shell(), "window.domAutomationController.send(clickCrossSiteLink());",
            &success));
        EXPECT_TRUE(success);
        EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
        EXPECT_EQ(url, observer.last_navigation_url());
        EXPECT_TRUE(observer.last_navigation_succeeded());
    }

    // The RenderFrameHost should not have changed unless site-per-process is
    // enabled.
    if (AreAllSitesIsolatedForTesting()) {
        EXPECT_NE(initial_rfh,
            static_cast<WebContentsImpl*>(shell()->web_contents())
                ->GetFrameTree()
                ->root()
                ->current_frame_host());
    } else {
        EXPECT_EQ(initial_rfh,
            static_cast<WebContentsImpl*>(shell()->web_contents())
                ->GetFrameTree()
                ->root()
                ->current_frame_host());
    }
}

// Ensure that browser side navigation handles navigation failures.
IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest, FailedNavigation)
{
    // Perform a navigation with no live renderer.
    {
        TestNavigationObserver observer(shell()->web_contents());
        GURL url(embedded_test_server()->GetURL("/title1.html"));
        NavigateToURL(shell(), url);
        EXPECT_EQ(url, observer.last_navigation_url());
        EXPECT_TRUE(observer.last_navigation_succeeded());
    }

    // Now navigate to an unreachable url.
    {
        TestNavigationObserver observer(shell()->web_contents());
        GURL error_url(
            net::URLRequestFailedJob::GetMockHttpUrl(net::ERR_CONNECTION_RESET));
        BrowserThread::PostTask(
            BrowserThread::IO, FROM_HERE,
            base::Bind(&net::URLRequestFailedJob::AddUrlHandler));
        NavigateToURL(shell(), error_url);
        EXPECT_EQ(error_url, observer.last_navigation_url());
        NavigationEntry* entry = shell()->web_contents()->GetController().GetLastCommittedEntry();
        EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    }
}

// Ensure that browser side navigation handles POST navigations correctly.
IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest, POSTNavigation)
{
    GURL url(embedded_test_server()->GetURL("/session_history/form.html"));
    GURL post_url = embedded_test_server()->GetURL("/echotitle");

    // Navigate to a page with a form.
    TestNavigationObserver observer(shell()->web_contents());
    NavigateToURL(shell(), url);
    EXPECT_EQ(url, observer.last_navigation_url());
    EXPECT_TRUE(observer.last_navigation_succeeded());

    // Submit the form.
    GURL submit_url("javascript:submitForm('isubmit')");
    NavigateToURL(shell(), submit_url);

    // Check that a proper POST navigation was done.
    EXPECT_EQ("text=&select=a",
        base::UTF16ToASCII(shell()->web_contents()->GetTitle()));
    EXPECT_EQ(post_url, shell()->web_contents()->GetLastCommittedURL());
    EXPECT_TRUE(shell()
                    ->web_contents()
                    ->GetController()
                    .GetActiveEntry()
                    ->GetHasPostData());
}

// Ensure that browser side navigation can load browser initiated navigations
// to view-source URLs.
IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest,
    ViewSourceNavigation_BrowserInitiated)
{
    TestNavigationObserver observer(shell()->web_contents());
    GURL url(embedded_test_server()->GetURL("/title1.html"));
    GURL view_source_url(content::kViewSourceScheme + std::string(":") + url.spec());
    NavigateToURL(shell(), view_source_url);
    EXPECT_EQ(url, observer.last_navigation_url());
    EXPECT_TRUE(observer.last_navigation_succeeded());
}

// Ensure that browser side navigation blocks content initiated navigations to
// view-source URLs.
IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest,
    ViewSourceNavigation_RendererInitiated)
{
    TestNavigationObserver observer(shell()->web_contents());
    GURL kUrl(embedded_test_server()->GetURL("/simple_links.html"));
    NavigateToURL(shell(), kUrl);
    EXPECT_EQ(kUrl, observer.last_navigation_url());
    EXPECT_TRUE(observer.last_navigation_succeeded());

    std::unique_ptr<ConsoleObserverDelegate> console_delegate(
        new ConsoleObserverDelegate(
            shell()->web_contents(),
            "Not allowed to load local resource: view-source:about:blank"));
    shell()->web_contents()->SetDelegate(console_delegate.get());

    bool success = false;
    EXPECT_TRUE(ExecuteScriptAndExtractBool(
        shell()->web_contents(),
        "window.domAutomationController.send(clickViewSourceLink());", &success));
    EXPECT_TRUE(success);
    console_delegate->Wait();
    // Original page shouldn't navigate away.
    EXPECT_EQ(kUrl, shell()->web_contents()->GetURL());
    EXPECT_FALSE(shell()
                     ->web_contents()
                     ->GetController()
                     .GetLastCommittedEntry()
                     ->IsViewSourceMode());
}

// Ensure that closing a page by running its beforeunload handler doesn't hang
// if there's an ongoing navigation.
IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest,
    UnloadDuringNavigation)
{
    content::WindowedNotificationObserver close_observer(
        content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
        content::Source<content::WebContents>(shell()->web_contents()));
    shell()->LoadURL(GURL("chrome://resources/css/tabs.css"));
    shell()->web_contents()->DispatchBeforeUnload();
    close_observer.Wait();
}

} // namespace content
